Skip to content

fix!(codegen): reset function outputs and default omitted arguments - #1958

Merged
Angus-Bethke-Bachmann merged 14 commits into
masterfrom
anbt/PRG-4940
Oct 6, 2026
Merged

Angus-Bethke-Bachmann merged 14 commits into
masterfrom
anbt/PRG-4940

Conversation

@Angus-Bethke-Bachmann

Copy link
Copy Markdown
Contributor

@ghaith or @volsa this one needs a careful review, it made some assumptions in codegen that seem right... but a few of the polymorphism tests have been changed. So a double check of this is probably necessary.

Problem: When a call left out an argument, the compiler passed the contents of a stack slot that nothing had written. The callee then saw whatever the previous call had left there. A VAR_OUTPUT of a function or method also started every call with the caller's current value, so a path that did not assign the output leaked that value back to the caller.

Solution: An argument that is left out, or written empty, now carries the declared default or zero. Every VAR_OUTPUT of a function or method is initialized like a local variable at the start of the call: codegen zero-fills it through the caller's address, and the init lowering adds the constructor call and the initializer assignment. Variable length array outputs keep the caller's bounds and are not reset. Reading an output before assigning it now gives its default, so callers no longer observe stale values. One known effect is that passing the same variable as an output and as an in-out of one call makes the body read zero; the book documents this.

Refs: PRG-4940

🤖 Generated with Claude Code

@github-actions

github-actions Bot commented Oct 1, 2026

Copy link
Copy Markdown

3 findings in 4m 31s for $0.73 between f7d42e8 (master) and faf4f52 (anbt/PRG-4940):

  • P2 book/user/language/functions.md:68: The claim that every output starts at its initial value or zero is too broad: codegen deliberately does not reset variable-length-array, REFERENCE TO, or alias outputs. Qualify the guarantee with these exceptions so readers are not led to expect their caller-owned storage to be cleared.
  • P2 book/user/language/methods-and-properties.md:38: This still tells readers they must supply every method parameter and says an omitted parameter without a default has no defined value. Method calls now allow any parameter to be omitted and codegen supplies its default or zero, so update this paragraph to describe that behavior.
  • P2 src/codegen/generators/pou_generator.rs:982: Runtime output defaults are evaluated without a function context: a method declaring VAR_OUTPUT p : POINTER TO Fb := ADR(THIS^); END_VAR now fails code generation. Static trace (not executed): maybe_get_constant_statement returns this address-unresolvable initializer, and generate_variable_initializer evaluates it with new_context_free, causing the THIS branch to return Cannot use 'this' without context. The lowered stack initializer can handle this expression, but the new prologue rejects it before that initializer runs.

@github-actions

github-actions Bot commented Oct 1, 2026 •

Copy link
Copy Markdown

Build Artifacts

🐧 Linux

Artifact Link Size
plc-aarch64 Download 43.3 MB
deb-aarch64 Download 30.8 MB
deb-x86_64 Download 38.4 MB
schema Download 0.0 MB
stdlib Download 32.4 MB
plc-x86_64 Download 43.5 MB

From workflow run

🪟 Windows

Artifact Link Size
stdlib.lib Download 4.0 MB
plc.exe Download 38.3 MB
stdlib.dll Download 0.1 MB

From workflow run

@github-actions

github-actions Bot commented Oct 1, 2026

Copy link
Copy Markdown

2 findings in 5m 10s for $0.72 between 5adf03e (master) and 75d231e (anbt/PRG-4940):

  • P2 book/user/language/functions.md:68: The aliasing warning says the shared input/in-out will read as zero, but an output with a nonzero initializer is reset to that initializer before the body runs. Describe the value as the output's initialized value (or zero when it has none), otherwise this example of the new behavior is inaccurate.
  • P2 book/user/language/methods-and-properties.md:38: The new claim that a method call may omit any parameter contradicts validation: src/validation/statement.rs:2484-2507 still requires every VAR_IN_OUT parameter and REFERENCE TO input, reporting E030 when one is omitted. For example, fb.m() with m declaring VAR_IN_OUT x : DINT fails compilation instead of receiving the documented initialized temporary; the same incorrect rule also appears in the updated technical codegen page (static trace, untested).

@github-actions

github-actions Bot commented Oct 2, 2026

Copy link
Copy Markdown

3 findings in 3m 35s for $0.75 between 10ead7b (master) and 5e0e6de (anbt/PRG-4940):

  • P2 book/technical/participants/06-init.md:144: This says outputs are zero-filled even when they have an initializer, but codegen initializes such outputs directly to the declared value (for example, the output-default snapshot stores 20 without first storing zero). Describe the entry behavior as applying the initializer or zero, with constructor work as needed, rather than requiring a zero-fill first.
  • P2 book/user/language/methods-and-properties.md:35: The new entry-reset behavior also applies to outputs of methods when the caller supplies storage, but this page only describes the temporary used when an output argument is omitted. State that a supplied method output is reset to its initializer or zero on entry (subject to the reference, alias, and VLA exceptions), since otherwise callers may expect an unassigned output to retain its prior value.
  • P2 src/codegen/generators/pou_generator.rs:944: Struct output defaults such as VAR_OUTPUT out : S := (x := 7); END_VAR reach this new initializer call with the by-reference pointer type hint, not S. Lowering decomposes the initializer into field assignments, so it does not correct the original aggregate's hint, and generate_literal_struct rejects it with Expected Struct-literal before those assignments run. This prevents functions and methods with struct output defaults from compiling (verified by static trace, not executed).

@ghaith

ghaith commented Oct 2, 2026

Copy link
Copy Markdown
Collaborator

@codex review

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 2, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-02T10:31:52.996517Z 5e0e6de Manual request
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5e0e6decf1

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +1705 to +1706
let temp = builder.build_alloca(temp_type, "empty_varinout")?;
builder.build_store(temp, temp_type.const_zero())?;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Apply type defaults to empty stateful in-out arguments

When a program or function-block call passes an empty VAR_IN_OUT whose target type has a nonzero default, for example TYPE T : DINT := 20 and fb(x := ), this branch always writes LLVM zero into the temporary. The function-call path uses get_initial_value, and the updated codegen documentation promises default-or-zero behavior, so the callee sees 0 instead of 20. Initialize the temporary from the parameter or type default before falling back to zero.

AGENTS.md reference: AGENTS.md:L42-L42

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The path at src/codegen/generators/expression_generator.rs#L1698-L1711 now hands the parameter entry to the same generate_empty_expression the function-call path uses, so the temporary starts at the parameter's default, then the type's default, then zero. The literal zero store remains only as a fallback when no parameter entry exists.

One finding for the reviewer: validation rejects this call shape from real source. inst(x := ) on a function block fails with E031, which demands a reference for an in-out. So the scenario is reachable only from codegen unit tests, which bypass validation. I could not write a lit test for it. Instead I added a unit test next to the existing one, program_empty_inout_assignment_takes_type_default, with an inline snapshot that shows store i32 20 into the temporary. The existing sibling snapshot changed only in the alloca's name.

&function_context,
debug,
)?;
self.generate_initialization_of_output_params(&pou_members, &local_index)?;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Update the POU internals page for output resets

This new initialization call makes every function or method output start at its default value, but book/technical/internals/00-pous.md still says that only locals and the return variable are initialized and shows scale IR with no store through %overflow. Update that paragraph and IR example so the technical book describes the behavior introduced here.

AGENTS.md reference: AGENTS.md:L42-L42

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

book/technical/internals/00-pous.md#L276 now says a function also starts each output at its initial value or zero through the caller's address. The scale IR example gained the load of the output pointer and the zero store after the return variable is zeroed, matching the real emission order. The closing sentence notes that flag holds the default even on a path that never assigns it.

@github-actions

github-actions Bot commented Oct 5, 2026

Copy link
Copy Markdown

5 findings in 4m 26s for $0.76 between 10ead7b (master) and 321b3c7 (anbt/PRG-4940):

  • P2 book/user/language/functions.md:68: The overlap warning says the callee would read the aliased variable as zero, but entry initialization writes the output's declared initial value when present. For an output initialized to a nonzero value, the stated result is wrong; say it is reset to its default (or zero).
  • P2 book/user/language/functions.md:84: The compiler now initializes an explicitly empty by-reference argument (=>/empty :=) to the parameter's default or zero in a temporary, but the user-facing call guidance never explains this behavior. Document the empty-argument case so callers know that it is defined rather than an uninitialized value.
  • P2 book/user/language/methods-and-properties.md:38: The new claim that a method call may omit any parameter contradicts validate_call, which still requires every VAR_IN_OUT parameter and every REFERENCE TO input. For example, calling fb.m() when m declares VAR_IN_OUT x : DINT; END_VAR fails with E030 instead of supplying a zeroed temporary, so the documented calling pattern does not compile; the same incorrect claim appears in the technical codegen page.
  • P2 book/user/language/methods-and-properties.md:38: This describes omitted method outputs as temporary but omits that supplied VAR_OUTPUT arguments are also reset to their initializer or zero on every call. The function page documents this only for functions, leaving the newly introduced method behavior uncovered in the user-facing method guidance.
  • P2 src/codegen/generators/pou_generator.rs:940: A struct-literal output initializer such as VAR_OUTPUT out : S := (x := 7); END_VAR is passed to the entry initializer even though it is already decomposed into lowered member assignments. Its resolved Assignment node retains the declaration's auto-pointer type hint, so generate_literal_struct rejects it with Expected Struct-literal before those assignments can run, preventing the function or method from compiling (static trace, not executed).

@github-actions

github-actions Bot commented Oct 5, 2026

Copy link
Copy Markdown

4 findings in 3m 32s for $0.70 between 10ead7b (master) and b54c8bd (anbt/PRG-4940):

  • P2 book/technical/internals/00-pous.md:276: This says every function output is initialized through the caller's address, but REFERENCE TO, AT alias, and variable-length-array outputs are not reset. Qualify the statement with those exceptions.
  • P2 book/technical/participants/06-init.md:144: The statement that codegen zero-fills every function/method output is too broad: it skips reset for REFERENCE TO, AT alias, and variable-length-array outputs. Qualify this initialization description with those exceptions.
  • P2 book/technical/pipeline/05-codegen.md:210: The output-reset description excludes only variable-length arrays, but codegen also skips resets for REFERENCE TO and AT alias outputs. Add those exceptions to keep this explanation consistent with the user-facing function rules and implementation.
  • P2 book/user/language/methods-and-properties.md:38: This says every supplied method output is reset, but codegen deliberately skips resets for variable-length-array, REFERENCE TO, and AT alias outputs. Add the same exceptions described in functions.md so method callers are not told these outputs are reset.

@github-actions

github-actions Bot commented Oct 5, 2026

Copy link
Copy Markdown

4 findings in 4m 48s for $0.58 between 10ead7b (master) and 15b7f84 (anbt/PRG-4940):

  • P2 book/technical/internals/00-pous.md:276: The statement that each function output starts at its initial value or zero omits the REFERENCE TO and AT alias outputs that codegen deliberately does not reset. Qualify the statement with those exceptions (and the VLA exception).
  • P2 book/technical/participants/06-init.md:144: This says every function/method output is zero-filled on entry, but codegen deliberately skips reset for REFERENCE TO and AT alias outputs as well as VLA outputs. Add those exceptions so the claimed initialization order matches the compiler.
  • P2 book/technical/pipeline/05-codegen.md:210: The output-reset description only exempts VLA outputs, but codegen also leaves REFERENCE TO and AT alias outputs untouched. Add these exceptions to avoid promising a reset the compiler does not perform.
  • P2 book/user/language/methods-and-properties.md:38: This promises that every supplied method output resets like a function output, but REFERENCE TO and AT alias outputs are not reset by codegen (and VLA outputs are also preserved). State the same exceptions as the functions page.

volsa
volsa previously approved these changes Oct 5, 2026
@github-actions

github-actions Bot commented Oct 6, 2026

Copy link
Copy Markdown

2 findings in 3m 57s for $0.54 between 10ead7b (master) and f7aad81 (anbt/PRG-4940):

  • P2 book/technical/pipeline/05-codegen.md:210: This says every output is reset, with only VLA outputs excluded, but codegen also skips resets for REFERENCE TO and AT-alias outputs. The same overstatement appears in technical/internals/00-pous.md and technical/participants/06-init.md; name these exceptions there as well so the mechanism description agrees with the behavior documented for users.
  • P2 compiler/plc_lowering/src/initializer.rs:265: Skipping every scalar output constructor loses address-valued type defaults, for example TYPE P : POINTER TO DINT := ADR(g); END_TYPE with VAR_OUTPUT out : P;. Static trace (not executed): DataTypeGenerator::generate_initial_value returns no initializer for pointer types, the new output-entry initializer stores null, and this guard prevents P__ctor(out) from applying ADR(g), so an empty function or method returns null instead of the declared default.

volsa
volsa previously approved these changes Oct 6, 2026
@github-actions

github-actions Bot commented Oct 6, 2026

Copy link
Copy Markdown

4 findings in 3m 51s for $0.67 between 46fc67d (master) and 45b2ab6 (anbt/PRG-4940):

  • P2 book/technical/participants/06-init.md:144: This says every output is zero-filled and then gets a constructor call, but scalar outputs are initialized directly to their value and deliberately get no constructor call. Distinguish scalar outputs from aggregates and pointer outputs, for which constructors are applicable.
  • P2 book/technical/pipeline/05-codegen.md:321: The omission rule incorrectly limits required REFERENCE TO parameters to inputs; method REFERENCE TO outputs are required too. State that no REFERENCE TO parameter of a method may be omitted.
  • P2 book/user/language/methods-and-properties.md:38: This says only REFERENCE TO inputs must be supplied, but validation requires every REFERENCE TO parameter of a method, including outputs. Clarify that all method REFERENCE TO parameters are required.
  • P2 compiler/plc_lowering/src/initializer.rs:257: Scalar outputs lose address-valued type defaults: with TYPE A : LWORD := ADR(g); END_TYPE and VAR_OUTPUT out : A, an empty function or method returns zero instead of ADR(g). Static trace (not executed): this guard suppresses A__ctor(out), while the entry reset finds no resolved constant for the address-valued default and stores zero; no variable-level initializer restores it. Keep runtime type-default initialization for scalar outputs and cover this case with a lit test.

@github-actions

github-actions Bot commented Oct 6, 2026

Copy link
Copy Markdown

1 finding in 4m 38s for $0.70 between 46fc67d (master) and b19b656 (anbt/PRG-4940):

  • P2 book/user/language/functions.md:68: The unconditional “Every call starts the output” claim also covers external functions, but their C implementations have no generated entry code to reset the caller's output. Scope this guarantee to compiler-generated function bodies so callers are not led to rely on stale-value clearing across the C interface.

@github-actions

github-actions Bot commented Oct 6, 2026

Copy link
Copy Markdown

1 finding in 3m 37s for $0.73 between 94669ef (master) and c3b0ed2 (anbt/PRG-4940):

  • P2 book/user/language/functions.md:92: This says a VAR_IN_OUT argument cannot be empty, but calls such as foo(inout1 := ) and prog(inout1 := ) are accepted and bind a temporary; this change initializes that temporary from the parameter/type default or zero. Document empty VAR_IN_OUT arguments as temporary-backed instead of declaring them invalid.

@Angus-Bethke-Bachmann
Angus-Bethke-Bachmann added this pull request to the merge queue Oct 6, 2026
Merged via the queue into master with commit 3cd74c3 Oct 6, 2026
22 checks passed
@Angus-Bethke-Bachmann
Angus-Bethke-Bachmann deleted the anbt/PRG-4940 branch October 6, 2026 10:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants